// ###########################################################################################################################
// Establishes the basic setup and integration of the DreamSpace module.
// ###########################################################################################################################

module DreamSpace.System {
    // =======================================================================================================================

    export interface IDelegate<TObj extends Object, TFunc extends DelegateFunction<Object>> extends Object, ISerializable {
        /** A read-only key string that uniquely identifies the combination of object instance and function in this delegate.
        * This property is set for new instances by default.  Calling 'getKey()' will update this string if necessary.
        */
        __key: string;
        /** The instance on which the associated function will be called. */
        object: TObj;
        /** The function to be called on the associated object. */
        func: IFunctionInfo;
        /** Invokes the delegate directly. Pass 'undefined'/'void 0'/'DreamSpace.UNDEFINED' for the first parameter, or a custom object context. */
        invoke: TFunc;
        /** Invoke the delegate with a fixed number of arguments (do not pass the object context ['this'] as the first parameter - use "invoke()" instead).
        * Note: This does not simply invoke "call()" on the function, but implements much faster calling patterns based on number of arguments, and browser type.
        */
        call: (...args: any[]) => any;
        /** Invoke the delegate using an array of arguments.
        * Note: This does not simply invoke "apply()" on the function, but implements much faster calling patterns based on number of arguments, and browser type.
        */
        apply(args: any[]): any;
        /** Invoke the delegate using a specific object context and array of arguments.
        * Note: This does not simply invoke "apply()" on the function, but implements much faster calling patterns based on number of arguments, and browser type.
        */
        apply(context: {}, args: any[]): any;
        /** If the 'object' or 'func' properties are modified directly, call this function to update the internal bindings. */
        update();
        /** Attempts to serialize the delegate.  This can only succeed if the underlying object reference is registered with 
        * an 'AppDomain', and the underlying function reference implements 'IFunctionInfo' (for the '$__name' property).  Be
        * careful when using function closures, as only the object ID and function name are stored.
        */
        getData(data: SerializedData): void;
        setData(data: SerializedData): void;
    }

    export interface DelegateFunction<TObj extends Object> { (_this: TObj, ...args): any };

    /** Returns a new 'Delegate' object. */
    export declare function Delegate<TObj extends Object, TFunc extends DelegateFunction<Object>>(object: TObj, func: TFunc): Delegate.$Type<TObj, TFunc>;
    /** Represents a function of a specific object instance.
    * Functions have no reference to any object instance when invoked statically.  This means when used as "handlers" (callbacks)
    * the value of the 'this' reference is either the global scope, or undefined (in strict mode).   Delegates couple the target
    * object instance (context of the function call [using 'this']) with a function reference.  This allows calling a function
    * on a specific object instance by simply invoking it via the delegate.
    * Note: If the target object is undefined, then 'null' is assumed and passed in as 'this'.
    */
    export module Delegate {
        /** Implements a more efficient '{function}.apply()' function when a small number of parameters are supplied. */
        export declare function fastApply(func: Function, context: {}, args: { [index: number]: any; length: number; }): any;

        export class $Type<TObj extends Object.$Type, TFunc extends DelegateFunction<Object>> extends Object.$Type implements IDelegate<TObj, TFunc> {

            private static __ctor = (() => {
                function makeCases(argsIndexStart: number, caseCountMax: number, func: Function, funcName: string, contextStr: string, argsStr: string): Function {
                    var ftext = func.toString();
                    var matchRegex = / ^.*case 1:[^;]*;/m;
                    var cases = "", argtext = "";
                    for (var i = argsIndexStart, n = argsIndexStart + caseCountMax; i < n; ++i)
                        cases += "case " + (1 + i) + ": return " + funcName + "(" + contextStr + (argtext += argsStr + "[" + i + "], ") + "this);\r\n";
                    return eval("(" + ftext.replace(/^.*case 1:[^;]*;/m, cases) + ")");
                }

                Delegate.fastApply = <any>makeCases(0, 20, function (func: Function, context: {}, args: { [index: number]: any; length: number; }) {
                    if (!arguments.length) throw Exception.error("Delegate.fastApply()", "No function specified.");
                    if (typeof func !== 'function') throw Exception.error("Delegate.fastApply()", "Function object expected.");
                    if (arguments.length == 1 || context == void (0) && (args == void (0) || !args.length)) return func();
                    switch (args.length) {
                        case 1: return func.call(context, args[0]);
                        default: return func.apply(context, args);
                    }
                }, "func.call", "context, ", "args");

                $Type.prototype.invoke = <any>makeCases(0, 20, function () {
                    var _this = <$Type<TObj, TFunc>>this;
                    if (!arguments.length) return _this.func(this.object, this);
                    var context = (arguments[0] === void 0) ? _this : arguments[0];
                    switch (arguments.length) {
                        case 1: return _this.func(context, arguments[1], this);
                        default: return _this.func.apply(this, [context].concat(arguments, this));
                    }
                }, "_this.func", "context, ", "arguments");

                var call = function () {
                    var _this = <$Type<TObj, TFunc>>this;
                    if (!arguments.length) return _this.func(this.object, this);
                    switch (arguments.length) {
                        case 1: return _this.func(_this.object, arguments[1], this);
                        default: return _this.func.apply(this, [_this.object].concat(arguments, this));
                    }
                };
                $Type.prototype.call = <any>((Browser.type != Browser.BrowserTypes.IE) ? makeCases(0, 20, call, "_this.func", "_this.object, ", "arguments") : makeCases(0, 20, call, "_this.__boundFunc", "", "arguments"));

                var apply = function (context, argsArray) { // (tests: http://jsperf.com/delegate-object-test/2)
                    var _this = <$Type<TObj, TFunc>>this;
                    if (arguments.length == 1) { // (only array given)
                        argsArray = context;
                        context = _this.object;
                    } else if (arguments.length > 1 && _this.apply != _this.__apply)
                        return _this.__apply(context, argsArray); // (only the non-bound version can handle context changes)
                    if (argsArray == void 0 || !argsArray.length) return _this.invoke(context, this);
                    switch (argsArray.length) {
                        case 1: return _this.func(context, argsArray[0], this);
                        default: return _this.func.apply(this, [context].concat(argsArray, this));
                    }
                };
                $Type.prototype.__apply = <any>makeCases(0, 20, apply, "_this.func", "context, ", "args"); // (keep reference to the non-bound version as a fallback for user defined contexts)
                $Type.prototype.apply = <any>((Browser.type != Browser.BrowserTypes.IE) ? $Type.prototype.__apply : makeCases(0, 20, apply, "_this.__boundFunc", "", "args")); // (note: bound functions are faster in IE)
            })();

            /** A read-only key string that uniquely identifies the combination of object instance and function in this delegate.
            * This property is set for new instances by default.  Calling 'getKey()' will update this string if necessary.
            */
            __key: string;

            /** The function to be called on the associated object. */
            func: IFunctionInfo;
            private __boundFunc: TFunc;
            private __functionText: string;

            /** Constructs a new delegate object.
            * @param {Object} The instance on which the associated function will be called.  This should be undefined/null for static functions.
            * @param {Function} The function to be called on the associated object.
            */
            constructor(public object: TObj, func: TFunc) {
                super();
                if (object === void 0) object = null;
                this.func = <any>func;
                this.update();
            }

            private static __THIS_REPLACE_REGEX = /([^A - Za-z$_\.])this([^A-Za-z$_])/gm;

            /** If the 'object' or 'func' properties are modified directly, call this function to update the internal bindings. */
            update() {
                if (typeof this.func != 'function')
                    Exception.error("Delegate", "The function value is not a function:\r\n {Delegate}.func = " + this.func, this.func);
                if (this.func.bind)
                    this.__boundFunc = this.func.bind(this, this.object); // (this can be faster in some cases [i.e. IE])
                this.__key = $Type.getKey(this.object, this.func); // (this also validates the properties first)
                return this;
            }

            private static __validate(callername: string, object: Object.$Type, func: DelegateFunction<Object>): boolean { // (returns 'true' if static)
                var isstatic: boolean = object === void 0 || object === null || !!(<IModuleInfo><any>object).$__fullname; // ('$__fullname' exists on modules and registered type objects)
                if (!isstatic && typeof object.__id != 'number')
                    throw Exception.error("Delegate." + callername, "The object for this delegate does not contain a numerical '__id' value (used as a global object reference for serialization), or '$__fullname' value (for static type references).  See 'AppDomain.registerObject()'.", this);
                return isstatic;
            }

            /** Invokes the delegate directly. Pass 'undefined'/'void 0'/'DreamSpace.UNDEFINED' for the first parameter, or a custom object context. */
            invoke: TFunc;

            /** Invoke the delegate with a fixed number of arguments (do not pass the object context ['this'] as the first parameter - use "invoke()" instead).
            * Note: This does not simply invoke "call()" on the function, but implements much faster calling patterns based on number of arguments, and browser type.
            */
            call: (...args: any[]) => any;

            /** Invoke the delegate using an array of arguments.
            * Note: This does not simply invoke "apply()" on the function, but implements much faster calling patterns based on number of arguments, and browser type.
            */
            apply: {
                /** Invoke the delegate using an array of arguments.
                * Note: This does not simply invoke "apply()" on the function, but implements much faster calling patterns based on number of arguments, and browser type.
                */
                (args: any[]): any;
                /** Invoke the delegate using a specific object context and array of arguments.
                * Note: This does not simply invoke "apply()" on the function, but implements much faster calling patterns based on number of arguments, and browser type.
                */
                (context: {}, args: any[]): any;
            };
            private __apply: (context: {}, args: any[]) => any; // (used as a fallback in case '__boundFunc' is used in the 'apply' routine)

            /** Attempts to serialize the delegate.  This can only succeed if the underlying object reference is registered with 
            * an 'AppDomain', and the underlying function reference implements 'IFunctionInfo' (for the '$__name' property).  Be
            * careful when using function closures, as only the object ID and function name are stored.
            */
            getData(data: SerializedData): void {
                var isstatic = $Type.__validate("getData()", this.object, this.func);
                if (!isstatic)
                    data.addValue("id", this.object.__id);
                data.addValue("ft", this.__functionText);
            }
            setData(data: SerializedData): void {
                var objid = data.getNumber("id");
                this.object = this.__appDomain.objects.getObject<TObj>(objid);
                this.__functionText = data.getValue("ft");
                this.update();
            }

            /** Creates and returns a string that uniquely identifies the combination of the object instance and function for
            * this delegate.  Since every delegate has a unique object ID across an application domain, key strings can help
            * prevent storage of duplicate delegates all pointing to the same target.
            * Note: The underlying object and function must be registered types first.
            * See 'AppDomain.registerClass()/.registerType()' for more information.
            */
            static getKey<TFunc extends DelegateFunction<Object>>(object: Object.$Type, func: TFunc): string {
                var isstatic = $Type.__validate("getKey()", object, func);
                var id = isstatic ? (object === void 0 || object === null ? '-1' : (<IModuleInfo><any>object).$__fullname) : object.__id.toString();
                return id + "," + (<IFunctionInfo><any>func).$__name; // (note: -1 means "global scope")
            }

            equal(value: any): boolean {
                return value !== void 0 && value !== null && value.constructor === Delegate
                    && (<$Type<any, any>>value).object === this.object && (<$Type<any, any>>value).func === this.func;
            }
        }
    }

    AppDomain.registerClass(Delegate, [DreamSpace, System]);

    // =======================================================================================================================
} // (DreamSpace)

// ###########################################################################################################################
